package net.towee.client.managers.impl;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import net.towee.client.event.core.DocumentDeletedEvent;
import net.towee.client.event.core.DocumentRatedEvent;
import net.towee.client.event.core.DocumentReceivedEvent;
import net.towee.client.event.core.NodeReceivedEvent;
import net.towee.client.managers.AccountHelper;
import net.towee.client.managers.DiscussionManager;
import net.towee.client.managers.DocumentsManager;
import net.towee.model.account.AccountIdentity;
import net.towee.model.account.AccountPermission;
import net.towee.model.discussion.DiscussionSubject;
import net.towee.model.repo.DocumentData;
import net.towee.model.repo.DocumentIdentity;
import net.towee.model.repo.DocumentPatch;
import net.towee.model.repo.DocumentType;
import net.towee.model.utils.Pair;
import net.towee.server.rpc.services.ToweeServiceAsync;

import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.common.collect.MapMaker;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.shared.HandlerManager;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.inject.Inject;

public class DocumentsManagerImpl implements DocumentsManager {
	private final Map<String, DocumentData> documentCache;
	private final Map<String, Map<Integer, DocumentData>> revisionsCache;
	private final Map<String, List<DocumentData>> searchCache;

	private final ToweeServiceAsync service;
	private final HandlerManager eventBus;
	private final AccountHelper accountHelper;
	private final DiscussionManager discManager;

	@Inject
	public DocumentsManagerImpl(ToweeServiceAsync service, HandlerManager eventBus,
			AccountHelper accountHelper, DiscussionManager discManager) {
		this.service = service;
		this.eventBus = eventBus;
		this.accountHelper = accountHelper;
		this.discManager = discManager;

		this.documentCache = new MapMaker().maximumSize(200).makeMap();

		this.searchCache = new MapMaker().maximumSize(40).expireAfterWrite(10, TimeUnit.MINUTES)
				.makeMap();

		this.revisionsCache = new MapMaker().maximumSize(10).expireAfterWrite(10, TimeUnit.MINUTES)
				.makeMap();

		createPushHandlers();
	}

	private void createPushHandlers() {
		service.onDocumentUpdate(null, null, new AsyncCallback<DocumentData>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(DocumentData result) {
				// Put to cache
				putToDocumentsCache(result);

				// Remove cached revisions
				revisionsCache.remove(result.getIdentity().getId());
				
				// Clear search cache
				searchCache.clear();

				// Fire an event
				eventBus.fireEvent(new DocumentReceivedEvent(result, false));
			}
		});

		service.onDocumentDelete(null, null, new AsyncCallback<List<DocumentIdentity>>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(List<DocumentIdentity> result) {
				for (DocumentIdentity doc : result)
					documentCache.remove(doc.getId());

				// Clear search cache
				searchCache.clear();
				
				eventBus.fireEvent(new DocumentDeletedEvent(result, false));
			}
		});

		service.onDocumentRated(null, null, new AsyncCallback<Pair<AccountIdentity, DocumentIdentity>>() {
			@Override
			public void onFailure(Throwable caught) {
			}

			@Override
			public void onSuccess(Pair<AccountIdentity, DocumentIdentity> result) {
				DocumentData doc = documentCache.get(result.getSecond().getId());

				if (doc != null)
					doc.getTups().addTup(result.getFirst());

				eventBus.fireEvent(new DocumentRatedEvent(result.getFirst(), result.getSecond(), false));
			}
		});
	}

	/**
	 * Put some document to cache
	 * 
	 * @param doc
	 */
	public void putToDocumentsCache(DocumentData doc) {
		Preconditions.checkNotNull(doc, "null document to put to cache");
		documentCache.put(doc.getIdentity().getId(), doc);
		((DiscussionManagerImpl) discManager).putDiscussionToCache(doc.getIdentity().getId(), doc.getDiscussion());

		// Update revisions
		if (revisionsCache.containsKey(doc.getIdentity().getId()))
			for (DocumentData d : revisionsCache.get(doc.getIdentity().getId()).values())
				d.setDiffs(doc.getDiffs());
	}

	@Override
	public void findDocuments(String str, List<String> groupId, String authorId, List<DocumentType> type,
			int offset, int limit, final AsyncCallback<List<DocumentData>> handler) {
		// Create identity of this request
		final String reqIdnt = str + (groupId != null ? groupId.hashCode() : "")
				+ (authorId != null ? authorId.hashCode() : "") + (type != null ? type.hashCode() : "")
				+ offset + limit;

		// Check search cache
		if (searchCache.containsKey(reqIdnt))
			handler.onSuccess(searchCache.get(reqIdnt));
		else
			service.findDocument(str, groupId, authorId, type, offset, limit,
					new AsyncCallback<List<DocumentData>>() {
						@Override
						public void onFailure(Throwable caught) {
						}

						@Override
						public void onSuccess(List<DocumentData> result) {
							Preconditions.checkNotNull(result, "fail documents search");

							// Put result to cache
							searchCache.put(reqIdnt, result);

							// Put results to documents cache
							for (DocumentData doc : result)
								putToDocumentsCache(doc);

							// Invoke handler
							handler.onSuccess(result);
						}
					});
	}

	@Override
	public void getDocuments(List<String> groupId, List<DocumentType> types, int offset, int limit,
			final AsyncCallback<List<DocumentData>> handler) {
		// Create identity of this request
		final String reqIdnt = "" + groupId.hashCode() + types.hashCode() + offset + limit;

		// Try to get from search cache
		if (searchCache.containsKey(reqIdnt))
			handler.onSuccess(searchCache.get(reqIdnt));
		else
			service.getDocuments(groupId, types, offset, limit, new AsyncCallback<List<DocumentData>>() {
				@Override
				public void onFailure(Throwable caught) {
					handler.onFailure(caught);
				}

				@Override
				public void onSuccess(List<DocumentData> result) {
					// Put to cache
					searchCache.put(reqIdnt, result);

					// Put results to documents cache
					for (DocumentData doc : result)
						putToDocumentsCache(doc);

					// Invoke handler
					handler.onSuccess(result);
				}
			});
	}

	@Override
	public void getDocument(String docId, final AsyncCallback<DocumentData> handler) {
		if (documentCache.containsKey(docId))
			handler.onSuccess(documentCache.get(docId));
		else
			service.getDocuments(docId, new AsyncCallback<DocumentData>() {
				@Override
				public void onFailure(Throwable caught) {
					handler.onFailure(caught);
				}

				@Override
				public void onSuccess(DocumentData result) {
					// Put to cache
					putToDocumentsCache(result);

					// Invoke handler
					handler.onSuccess(result);
				}
			});
	}

	@Override
	public void saveDocument(DocumentData doc, String parentNodeId,
			final AsyncCallback<DocumentData> handler) {
		Preconditions.checkArgument(accountHelper.hasPermissions(doc.getIdentity().getGroup().getId(),
				AccountPermission.CAN_ADD_DOCUMENT));

		Preconditions.checkNotNull(doc, "null doc for saving");
		Preconditions.checkNotNull(doc.getIdentity(), "null doc identity for saving");
		Preconditions.checkNotNull(doc.getIdentity().getGroup(), "null group identity");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(doc.getIdentity().getName()),
				"null document name");

		// Save document
		service.saveDocument(doc, parentNodeId, new AsyncCallback<DocumentData>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(DocumentData result) {
				Preconditions.checkNotNull(result, "fail save document");

				// Put document to cache
				putToDocumentsCache(result);
				
				// Clear search cache
				searchCache.clear();

				// Fire an event
				eventBus.fireEvent(new DocumentReceivedEvent(result, true));
				handler.onSuccess(result);
			}
		});
	}

	@Override
	public void changeDocument(DocumentData doc, String explain, final AsyncCallback<DocumentData> handler) {
		Preconditions.checkArgument(
				doc.getIdentity().getAuthor().equals(accountHelper.getAccount().getAccountIdentity())
						|| accountHelper.hasPermissions(doc.getIdentity().getGroup().getId(),
								AccountPermission.CAN_CHANGE_OTHER_DOCUMENT), "no permission");

		Preconditions.checkNotNull(doc, "null document");
		Preconditions.checkNotNull(doc.getIdentity(), "null identity");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(doc.getIdentity().getName()), "null doc name");
		Preconditions.checkArgument(!Strings.isNullOrEmpty(doc.getIdentity().getId()), "null doc id");
		Preconditions.checkNotNull(doc.getIdentity().getGroup(), "null group identity");

		service.changeDocument(doc, explain, new AsyncCallback<DocumentData>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(DocumentData result) {
				Preconditions.checkNotNull(result, "fail change document");

				// Put document to cache
				putToDocumentsCache(result);

				// Fire an event
				eventBus.fireEvent(new DocumentReceivedEvent(result, true));
				handler.onSuccess(result);
			}
		});
	}

	@Override
	public void tupDocument(final String docId, final AsyncCallback<Boolean> handler) {
		Preconditions.checkArgument(!Strings.isNullOrEmpty(docId), "null doc id");

		service.tupDocument(docId, new AsyncCallback<Boolean>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(Boolean result) {
				Preconditions.checkArgument(result, "fail tup document");

				// Try tup in local stored document
				DocumentData doc = documentCache.get(docId);

				if (doc != null) {
					doc.getTups().addTup(accountHelper.getAccount().getAccountIdentity());

					// Fire an event
					eventBus.fireEvent(new DocumentRatedEvent(accountHelper.getAccount()
							.getAccountIdentity(), doc.getIdentity(), true));

					handler.onSuccess(true);
				}
			}
		});
	}

	@Override
	public void getDocumentRevision(final String docId, final int revNumber,
			final AsyncCallback<DocumentData> handler) {
		getDocument(docId, new AsyncCallback<DocumentData>() {
			@Override
			public void onFailure(Throwable caught) {
				handler.onFailure(caught);
			}

			@Override
			public void onSuccess(DocumentData doc) {
				Preconditions.checkArgument(doc.getDiffs().size() > revNumber,
						"revision number out of range");

				// Get cached revisions
				Map<Integer, DocumentData> revisions = revisionsCache.get(docId);

				if (revisions == null) {
					revisions = new HashMap<Integer, DocumentData>();
					revisionsCache.put(docId, revisions);
				}
				// Try to get revision from cache
				if (revisions.containsKey(revNumber)) {
					handler.onSuccess(revisions.get(revNumber));
				} else if (revNumber == doc.getDiffs().size() - 1)
					handler.onSuccess(doc);

				else {
					// To get needed revision we MUST create all prev revisions
					// -1(ORIGINAL) <-- 0 <-- 1 <-- 2 <-- 3(CURRENT_REVISION)
					List<String> neededPatchs = new ArrayList<String>();
					int startRevision = revNumber;

					for (int i = revNumber + 1; i < doc.getDiffs().size(); i++) {
						neededPatchs.add(doc.getDiffs().get(i).getPatchId());
						startRevision++;

						if (revisions.containsKey(i)) {
							doc = revisions.get(i);
							break;
						}
					}

					// For async callback
					final int startRevisionF = startRevision;
					final DocumentData lastCreatedRevision = doc;

					// Get needed patches
					service.getDocumentPatchs(neededPatchs,
							new AsyncCallback<Map<String, DocumentPatch>>() {
								@Override
								public void onFailure(Throwable caught) {
									handler.onFailure(caught);
								}

								@Override
								public void onSuccess(Map<String, DocumentPatch> result) {
									Map<Integer, DocumentData> revisions = revisionsCache.get(docId);
									DocumentData curDoc = lastCreatedRevision;

									// Get all revisions to needed
									for (int i = startRevisionF; i > revNumber; i--) {
										// Patch current for getting prev
										curDoc = result.get(
												lastCreatedRevision.getDiffs().get(i).getPatchId())
												.patchDocument(curDoc);

										// Put prev revision to cache
										revisions.put(i - 1, curDoc);
									}

									// Invoke handler
									handler.onSuccess(curDoc);
								}
							});
				}
			}
		});
	}

	@Override
	public void getRecentDocumentsCount(List<String> groupId, List<DocumentType> types,
			AsyncCallback<Integer> handler) {
		service.getRecentDocumentsCount(groupId, types, handler);
	}
}
